using System;
using LuaCSharpFunction;
using System.Globalization;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
namespace Lua4Mono
{
	public class Lua:System.Dynamic.DynamicObject,IDisposable
	{
		#region "static"
		static HashSet<string> _keyWrods=new HashSet<string>(new string[]{"DoString","Dispose"});
		#endregion
		HashSet<LuaInvoke> _inovkes = new HashSet<LuaInvoke>();
		object _SyncRoot=new object();
		public IntPtr _lua;
		public Lua (bool IsOpenLibs=false)
		{
			_lua =LuaAuxLib.luaL_newstate();
			if(IsOpenLibs)
				LuaAuxLib.luaL_openlibs(_lua);
			_inovkes = new HashSet<LuaInvoke>();
		}
		
		~Lua()
		{
			if(!_isDisposed)
			{
				lock(_SyncRoot)
				{
					if(!_isDisposed)
					{
						Lua51.lua_close(_lua);
						_isDisposed =true;
					}
				}
			}
		}
		
		bool _isDisposed;
		
		void luaReady()
		{
			if(_lua==IntPtr.Zero||_isDisposed==true)
				throw new Exception("Lua:"+_lua+" is null !!");
		}
		
		public override bool TrySetMember (System.Dynamic.SetMemberBinder binder, object value)
		{
			luaReady();
			if(_keyWrods.Contains(binder.Name))
				return false;
			if((value as MethodInfo)!=null)
			{
				LuaInvoke li = new LuaInvoke((MethodInfo)value);
				this._inovkes.Add(li);
				Tools.Set(_lua,binder.Name,li.Handler);
				return true;
			}
			Tools.Set(_lua,binder.Name,value);
			return true;
		}
		
		public override bool TryGetMember (System.Dynamic.GetMemberBinder binder, out object result)
		{
			luaReady();
			result = Tools.Get(_lua,binder.Name);
			return true;
		}
		
		public override bool TryInvokeMember (System.Dynamic.InvokeMemberBinder binder, object[] args, out object result)
		{
			luaReady();
			result =Tools.Invoke(_lua,binder.Name,args);
			return true;
		}
		
		public object[] DoString(string luaSrc)
		{
			luaReady();
			
			if(_isDisposed)
				return null;
			lock(_SyncRoot)
			{
				if(LuaMacros.luaL_dostring(_lua,luaSrc))
				{  
					int count =Lua51.lua_gettop(_lua);
					object[] rtn =new object[count];
					for(int a=0;a<count;a++)
					{
						rtn[a]=Tools.Pop(_lua);
					}
					return rtn;
				}
			}
			return null;
		}
		
		public T DoString<T>(string luaSrc,int idx=0)
		{
			object[] rtn =DoString(luaSrc);
			if(rtn!=null && rtn.Length>idx)
			{
				return (T)rtn[idx];
			}
			return default(T);
		}
		
		#region IDisposable implementation
		public void Dispose ()
		{
			if(!_isDisposed)
			{
				lock(_SyncRoot)
				{
					if(!_isDisposed)
					{
						Lua51.lua_close(_lua);
						_lua =IntPtr.Zero;
						_isDisposed =true;
					}
				}
			}
		}
		#endregion
		
		public object[] GetStack()
		{
			luaReady();
			int stackLength =Lua51.lua_gettop(_lua);
			if(stackLength==0)
				return new object[0];
			object[] rtn = new object[stackLength];
			for(int a=0;a<stackLength;a++)
			{
				rtn[a]=Tools.Read(_lua,a+1);
			}
			return rtn;
		}
		
	}
	
	public class LuaInvoke
	{
		MethodInfo _mif;
		ParameterInfo[] _pifs;
		LuaCSsharpFunction _lcf;
		public LuaInvoke(MethodInfo mif)
		{
			if(mif ==null)
				throw new Exception ("mif is null");
			if(!mif.IsStatic)
				throw new Exception ("static method only!");
			if(mif.IsGenericMethod)
				throw new Exception("Can not use a generic method");
			
			_mif =mif;
			_pifs =_mif.GetParameters();
			foreach(var pif in _pifs)
			{
				TypeCode tc =Type.GetTypeCode(pif.ParameterType);
				if(tc==TypeCode.Object)
					throw new Exception("Parameter Can not be TypeCode.Object");
			}
			_lcf =new LuaCSsharpFunction(Invoke);
		}
		
		public int Invoke(IntPtr L)
		{
			int baseStack =1;
			baseStack +=_pifs.Length;
			if(Lua51.lua_checkstack(L,baseStack)==0)
				throw new Exception("stack overflow");
			if(baseStack ==Lua51.lua_gettop(L))
				throw new Exception("parameter not enough!");
			object[] parr=new object[baseStack -1];
			for(int a=0;a<parr.Length;a++)
			{
				parr[parr.Length-a-1]=Tools.Pop(L,_pifs[parr.Length-a-1].ParameterType);
			}
			object rtns =_mif.Invoke(null, parr);
			Lua51.lua_settop(L,0);
			return Tools.Push(L,rtns);
		}
		
		public LuaCSsharpFunction Handler
		{
			get{
				
				return _lcf;
			}
		}
	}
	
	public class Tools
	{
		
		public static int Push(IntPtr L,object obj)
		{
			if(obj ==null)
				return 1;
			IConvertible conv =obj as IConvertible;
			if(conv !=null)
			{
				TypeCode tp =conv.GetTypeCode();
				
				switch (tp)
				{
					case TypeCode.DBNull:
					case TypeCode.Empty :
						Lua51.lua_pushnil(L);
						return 1;
					case TypeCode.SByte:
					case TypeCode.Byte:
					case TypeCode.Int16:
					case TypeCode.Int32:
					case TypeCode.Int64:
					case TypeCode.Double:
					case TypeCode.Decimal:
					case TypeCode.Single:
					case TypeCode.UInt16:
					case TypeCode.UInt32:
					case TypeCode.UInt64:
						Lua51.lua_pushnumber(L,conv.ToDouble(CultureInfo.InvariantCulture ));
						return 1;
					case TypeCode.Char:
					case TypeCode.String:
					case TypeCode.DateTime:
						Lua51.lua_pushstring(L,obj.ToString());
						return 1;
					case TypeCode.Boolean:
						Lua51.lua_pushboolean(L,(bool)obj);
						return 1;
				}
			}
			var a =obj as LuaCSsharpFunction;
			if(a!=null)
			{
				LuaMacros.lua_pushcfunction(L,a);
				return 1;
			}
			if(obj as IDictionary !=null)
			{
				Tools.NewDicTable(L,(IDictionary)obj);
				return ((IDictionary)obj).Count;
			}
			if(obj is Array)
			{
				Tools.NewArrayTable(L,(Array)obj);
				return ((Array)obj).Length;
			}
			return -1;
		}
		
		public static T Pop<T>(IntPtr L)
		{
			object obj =Pop(L);
			
			return (T)obj;
		}
		
		public static object Pop(IntPtr L,Type type)
		{
			object obj =Pop (L);
			if(obj.GetType().Equals(type))
				return obj;
			if((obj as IConvertible)==null )
				return null;
			return Convert.ChangeType(obj,Type.GetTypeCode(type));
		}
		
		public static object Pop(IntPtr L)
		{
			int top =Lua51.lua_gettop(L);
			
			if(top==0)
				return null;
			object rtn =null;
//			
//			switch(Lua51.lua_type(L,-1))
//			{
//			case LUA_TYPES.LUA_TBOOLEAN:
//				rtn = Lua51.lua_toboolean(L,-1)!=0;
//				break;
//			case LUA_TYPES.LUA_TNUMBER:
//				rtn = Lua51.lua_tonumber(L,-1);
//				break;
//			case LUA_TYPES.LUA_TNIL:
//			case LUA_TYPES.LUA_TONOE:
//				rtn	= null;
//				break;
//			case LUA_TYPES.LUA_TSTRING:
//				rtn = Lua51.lua_tostring(L,-1);
//				break;
//			case LUA_TYPES.LUA_TFUNCTION:
//				rtn =Lua51.lua_tocfunction(L,-1);
//				break;
//			case LUA_TYPES.LUA_TTABLE:
//				rtn =GetTable(L);
//				break;
//			}
			rtn =Read(L);
			LuaMacros.lua_pop(L,1);
			return rtn;
		}
		
		public static object Read(IntPtr L,int pos=-1)
		{
			int top =Lua51.lua_gettop(L);
			
			if(top==0)
				return null;
			object rtn =null;
			
			switch(Lua51.lua_type(L,pos))
			{
			case LUA_TYPES.LUA_TBOOLEAN:
				rtn = Lua51.lua_toboolean(L,pos)!=0;
				break;
			case LUA_TYPES.LUA_TNUMBER:
				rtn = Lua51.lua_tonumber(L,pos);
				break;
			case LUA_TYPES.LUA_TNIL:
			case LUA_TYPES.LUA_TONOE:
				rtn	= null;
				break;
			case LUA_TYPES.LUA_TSTRING:
				rtn = Lua51.lua_tostring(L,pos);
				break;
			case LUA_TYPES.LUA_TFUNCTION:
				rtn =Lua51.lua_tocfunction(L,pos);
				break;
			case LUA_TYPES.LUA_TTABLE:
				rtn =GetTable(L);
				break;
			}
			return rtn;
		}
		
		public static object GetTable(IntPtr L)
		{
			int oldtop =Lua51.lua_gettop(L);
			List<object> arr=null;
			Dictionary<string,object> dic=null;
			Lua51.lua_pushnil(L);
			if(Lua51.lua_next(L,-2)!=0)
			{
				if(Lua51.lua_type (L,-2)== LUA_TYPES.LUA_TNUMBER)
				{
					arr = new List<object>();
					arr.Add(Pop(L));
				}
				else
				{
					dic = new Dictionary<string, object>();
					dic.Add(Lua51.lua_tostring(L,-2),Pop(L));
				}
			}
			while(Lua51.lua_next(L,-2)!=0)
			{
				if(dic==null)
					arr.Add(Pop(L));
				else
					dic.Add(Lua51.lua_tostring(L,-2),Pop(L));
			}
			Lua51.lua_settop(L,oldtop);
			if(dic ==null)
				return arr.ToArray();
			return dic;
		}
		
		public static object Get(IntPtr L,string key)
		{
			LuaMacros.lua_getglobal(L,key);
			return Pop (L);
		}
		
		public  static void Set(IntPtr L,string key,object obj)
		{
		
			if(Push(L,obj)==-1)
				throw new Exception("Can't push a unhandled type to lua");
			LuaMacros.lua_setglobal(L,key);
		}
			
		public static object[] Invoke(IntPtr L,string key,object[] args)
		{
			LuaMacros.lua_getglobal(L,key);
			if(!LuaMacros.lua_isX(L,-1, LUA_TYPES.LUA_TFUNCTION))
				return null;
			int state =0;
			if(args!=null)
			{
				foreach(object arg in args)
				{
					Push(L,arg);
				}
				state = Lua51.lua_pcall(L,args.Length,-1,0);
			}
			else
			{
				state = Lua51.lua_pcall(L,0,-1,0);
			}
			if(((int)THREAD_STATUS.LUA_OK)==state)
			{
				int count =Lua51.lua_gettop(L);
				if(count ==1)
				{
					return new object[]{Pop(L)};
				}
				object[] rtns =new object[count];
				for(int a=0;a<count;a++)
				{
					rtns[a]=Pop(L);
				}
				return rtns;
			}
			return null;
		}
		
		public static void NewArrayTable(IntPtr L,Array arr)
		{
			Lua51.lua_newtable(L);
			{
				for(int a=0;a<arr.Length;a++)
				{
					Push(L,arr.GetValue(a));
					Lua51.lua_rawseti(L,-2,a+1);     
				}
			}
		
		}
		
		public static void NewDicTable(IntPtr L,IDictionary dic)
		{
			Lua51.lua_newtable(L);
			{
			foreach(var sub in ((IDictionary)dic).Keys)
				{
					if(sub.GetType().Equals(typeof(string)))
					{
						Push(L,sub.ToString());
						Push (L,dic[sub]);
						Lua51.lua_settable(L,-3);
					}
				}
			}
			
		}
		
		
		
		
	}
}

